﻿using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace Quail.Parallel
{
    /// <summary>
    /// Used to mananage mutiple <see cref="ParallelTestWorker{TShell,TTestData}"/> objects for automed tests.  
    /// The <see cref="ParallelTestManager{TShell,TTestData}"/> class can be used directly or can be used as a base class.
    /// </summary>
    /// <typeparam name="TShell">The type of test shell to use</typeparam>
    /// <typeparam name="TTestData">The type of test data for each worker to consume</typeparam>
    public class ParallelTestManager<TShell, TTestData> : IDisposable
        where TShell : ITestShell
    {
        private readonly object _syncRoot = new object();
        private readonly ConcurrentDictionary<int, TShell> _subShells;
        private readonly ConcurrentQueue<TTestData> _itemQueue;
        private readonly List<ParallelTestWorker<TShell, TTestData>> _workers;
        private Task _rootTask;
        private int _maxNumberOfWorkers;
        private string _paralellTestName;

        /// <summary>
        /// Initializes a new instance of the <see cref="ParallelTestManager{TShell,TTestData}"/> class.
        /// </summary>
        /// <param name="rootShell">The root shell for the manager.  The root shell can be used by the FillWorkerQueue method and is also used by the first worker.  The root shell should already be started when it is passed in.</param>
        /// <param name="maxNumberOfWorkers">The maximum number of workers to use to perform the tests.</param>
        public ParallelTestManager(
            ITestShell rootShell,
            int maxNumberOfWorkers)
        {
            // Set up field values
            MaxNumberOfWorkers = maxNumberOfWorkers;
            RootShell = rootShell;
            UseRootShellAsWorker = true;

            if (RootShell is TShell)
                CreateParallelTestShell = idx => (TShell)RootShell.CreateNewTestShell(ParallelTestName + "[" + idx + "]");

            _rootTask = RootShell.ActiveTask;
            _subShells = new ConcurrentDictionary<int, TShell>();
            _workers = new List<ParallelTestWorker<TShell, TTestData>>();
            _itemQueue = new ConcurrentQueue<TTestData>();

            // Set up delegates
            GetNextWorkItemMethod = GetNextWorkItemFromWorkQueue;
        }

        ~ParallelTestManager()
        {
            Dispose(false);
        }

        /// <summary>
        /// Gets or sets the name to use for the parallel test shells
        /// </summary>
        public string ParallelTestName
        {
            get
            {
                return _paralellTestName ?? RootShell.TestName;
            }
            set
            {
                if (IsTestRunning)
                    throw new ParallelTestAlreadyRunningException();

                _paralellTestName = value;
            }
        }

        /// <summary>
        /// Gets or sets the method to execute during the test run
        /// </summary>
        public Action<TShell, TTestData> TestMethod { get; set; }

        /// <summary>
        /// Gets or sets the method to execute when a test worker is complete
        /// </summary>
        public Action<TShell> TestComplete { get; set; }

        /// <summary>
        /// Gets or sets the method to return the next work item
        /// </summary>
        public Func<ParallelTestContext<TShell, TTestData>, TTestData> GetNextWorkItemMethod { get; set; }

        /// <summary>
        /// Gets or sets a value indicating whether or not all all workers should stop when any one worker throws an exception
        /// </summary>
        public bool StopAllWorkersWhenExceptional { get; set; }

        /// <summary>
        /// Gets the max number of workers
        /// </summary>
        public int MaxNumberOfWorkers
        {
            get
            {
                lock (_syncRoot)
                    return _maxNumberOfWorkers;
            }

            set
            {
                lock (_syncRoot)
                {
                    if (IsTestRunning)
                        throw new ParallelTestAlreadyRunningException();

                    _maxNumberOfWorkers = value;
                }
            }
        }

        /// <summary>
        /// Gets a reference to the root shell instance
        /// </summary>
        public ITestShell RootShell
        {
            get; private set;
        }

        /// <summary>
        /// Gets or sets a value indicating whether to Dispose the RootShell
        /// </summary>
        public bool DisposeRootShell
        {
            get; set;
        }

        /// <summary>
        /// Gets or sets the create new test shell method.  Obsolete, use CreateParallelTestShell instead
        /// </summary>
        /// <value>The create new test shell.</value>
        [Obsolete]
        public Func<TShell> CreateNewTestShell
        {
            get { return () => CreateParallelTestShell(0); }
            set { CreateParallelTestShell = idx => value(); }
        }

        /// <summary>
        /// Gets or sets the create new test shell method.
        /// First parameter is the thread index
        /// </summary>
        /// <value>The create new test shell.</value>
        public Func<int, TShell> CreateParallelTestShell
        {
            get;
            set;
        }


        /// <summary>
        /// Gets or sets a value indicating whether to use the root shell as worker.
        /// </summary>
        public bool UseRootShellAsWorker
        {
            get; set;
        }

        /// <summary>
        /// Gets the root task.
        /// </summary>
        /// <value>The root task.</value>
        public Task RootTask
        {
            get
            {
                return _rootTask;
            }

            set
            {
                lock (_syncRoot)
                {
                    if (IsTestRunning)
                        throw new ParallelTestAlreadyRunningException();
                    _rootTask = value;
                }
            }
        }


        /// <summary>
        /// Gets a value indicating whether the tests are currently running
        /// </summary>
        public bool IsTestRunning
        {
            get; private set;
        }

        /// <summary>
        /// Fills the work queue, creates the workers, starts the workers, and waits for the workers to finish
        /// </summary>
        public void RunTests()
        {
            lock (_syncRoot)
            {
                if (IsTestRunning)
                    throw new ParallelTestAlreadyRunningException();
                IsTestRunning = true;
            }
            try
            {
                var task = _rootTask.BeginTask(ParallelTestName + ".RunTests");
                try
                {
                    CreateWorkers();
                    var options = new ParallelOptions {MaxDegreeOfParallelism = MaxNumberOfWorkers};
                    System.Threading.Tasks.Parallel.ForEach(_workers, options, StartWorker);
                    task.Finish();
                }
                catch (Exception e)
                {
                    task.Finish(e);
                    throw;
                }
            }
            finally
            {
                IsTestRunning = false;
            }
        }

        /// <summary>
        /// Call to clean up the test shells.
        /// </summary>
        public void FinishTestShells()
        {
            foreach (var s in _subShells.Values)
                s.Dispose();

            _subShells.Clear();
            _workers.Clear();
        }

        /// <summary>
        /// Disposes any shells held by the manager (but not the root shell)
        /// </summary>
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        protected virtual void Dispose(bool disposing)
        {

            if (disposing)
            {
                FinishTestShells();

                if (DisposeRootShell)
                    RootShell.Dispose();
            }
        }

        /// <summary>
        /// Adds an item to the work queue
        /// </summary>
        /// <param name="item">the item to add</param>
        public void EnqueueWorkItem(TTestData item)
        {
            _itemQueue.Enqueue(item);
        }

        /// <summary>
        /// Creates a worker for use by the manager.
        /// If deriving from this class, you can override this method.  Otherwise a CreateWorker action must be passed into the public constructor.
        /// </summary>
        /// <returns>An instance of <see cref="ParallelTestWorker{TShell,TTestData}"/>.</returns>
        protected virtual ParallelTestWorker<TShell, TTestData> CreateWorker()
        {
            return new ParallelTestWorker<TShell, TTestData>((shell, data) => TestMethod(shell, data));
        }

        /// <summary>
        /// Creates and starts a worker shell for use by the manager
        /// If deriving from this class, you can override this method.  Otherwise a StartShell action must be passed into the public constructor.
        /// </summary>
        /// <param name="threadIndex">index of the thread</param>
        /// <returns>A new shell which has been started and is ready for use.</returns>
        protected virtual TShell StartShell(int threadIndex)
        {
            if (_rootTask == null)
                throw new NullReferenceException("_rootTask is null - it shouldn't be!");

            if (CreateParallelTestShell == null)
                throw new NullReferenceException("CreateParallelTestShell must be defined for ParallelTestManager");

            return WaitParameters.RetryTimeout(
                _rootTask,
                "CreateWorkerTestShell",
                t =>

                    {
                        var shell = CreateParallelTestShell(threadIndex);
                        try
                        {
                            shell.Start(_rootTask);

                            // set the timeout to be the same
                            shell.TestSession.Browser.Timeout = _rootTask.TestSession.Browser.Timeout;

                            shell.TestSession.Browser.SilentLogging = _rootTask.TestSession.Browser.SilentLogging;

                            // copy the userstate
                            shell.SharedParallelState = RootShell.SharedParallelState;
                            return shell;
                        }
                        catch (Exception)
                        {
                            shell.Dispose(); // we aren't using the shell any more, so we can dispose it
                            throw;
                        }
                    }
                );
        }

        /// <summary>
        /// Populates the list of workers for use by the manager.
        /// </summary>
        private void CreateWorkers()
        {
            while (_workers.Count < MaxNumberOfWorkers)
            {
                var worker = CreateWorker();
                worker.StopWhenOtherWorkersAreExceptional = StopAllWorkersWhenExceptional;
                _workers.Add(worker);
            }
        }

        /// <summary>
        /// Gets the next item from the work queue
        /// </summary>
        /// <param name="context">the test context for the work queue</param>
        /// <returns>the next item, or null if no more items are available</returns>
        private TTestData GetNextWorkItemFromWorkQueue(ParallelTestContext<TShell, TTestData> context)
        {
            TTestData result;
            return _itemQueue.TryDequeue(out result) ? result : default(TTestData);
        }

        /// <summary>
        /// Starts the given worker by assinging it a shell and invoking the worker's Start method.
        /// </summary>
        /// <param name="worker">The worker to start.</param>
        /// <param name="loopState">The state of the parallel loop.  Used to determine when another worker has thrown and exception.</param>
        /// <param name="index">The index of the worker to be started.</param>
        private void StartWorker(ParallelTestWorker<TShell, TTestData> worker, ParallelLoopState loopState, long index)
        {
            if (loopState.IsExceptional && StopAllWorkersWhenExceptional)
                return;
            var context = new ParallelTestContext<TShell, TTestData>(this, GetNextWorkItemMethod, () => GetShell((int)index));
            worker.Start(context, loopState);
        }

        /// <summary>
        /// Gets a shell by index.  If the shell is already started, it returns the existing shell.  
        /// If the shell is not already started, it creates a new shell using the StartShell method and returns it.
        /// </summary>
        /// <param name="index">The index of the worker requesting the shell.  The worker with an index of 0 recieves the root shell.</param>
        /// <returns>A test shell that has been started and it ready for use.</returns>
        private TShell GetShell(int index)
        {
            if (index == 0 && UseRootShellAsWorker && RootShell is TShell)
                return (TShell) RootShell;

            var shell = _subShells.GetOrAdd(index, StartShell);
            return shell;
        }

        /// <summary>
        /// Exception class for the ParallelTestManager
        /// </summary>
        public class ParallelTestManagerException : LogStatusException
        {
            ///<summary>
            /// Default constructor
            ///</summary>
            ///<param name="message"></param>
            public ParallelTestManagerException(string message) : base(LogStatus.Fail, message)
            {
                
            }
        }

        /// <summary>
        /// Exception thrown when setting values that shouldn't be set while the test is running
        /// </summary>
        public class ParallelTestAlreadyRunningException : ParallelTestManagerException
        {
            ///<summary>
            /// Default constructor
            ///</summary>
            ///<param name="message"></param>
            public ParallelTestAlreadyRunningException()
                : base("Operation invalid.  Tests are in progress.")
            {
            }            
        }
    }
}
